﻿using ComputedTable.Version10;
using Microsoft.VisualBasic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace MortgageCalculator.Demo
{
    public class MortgageCalculator
    {
        private const string CsvFile = @"c:\temp\AmortizationTableCalculator.csv";
        private const string XmlFile = @"c:\temp\AmortizationTableCalculator.xml";
        private ICalculatorFactory<int, string, MortgageCalculatorOperationContext> calculatorFactory;

        public MortgageCalculator(ICalculatorFactory<int, string, MortgageCalculatorOperationContext> calculatorFactory = null)
        {
            if (calculatorFactory != null)
            {
                this.calculatorFactory = calculatorFactory;
            }
            else
            {
                this.calculatorFactory = this.CreateCalculatorFactory();
            }
        }

        public MortgageCalculatorOutput Calculate(MortgageCalculatorInput input)
        {
            var output = new MortgageCalculatorOutput();
            output.MonthlyLoanPayments = (decimal)Financial.Pmt(
                (double)(input.InterestRate / 12),
                input.DurationOfLoan,
                -(double)input.LoanAmount);
            output.MonthlyLoanPayments = Decimal.Round(output.MonthlyLoanPayments);
            output.TotalMonthlyPayments = Decimal.Round((output.MonthlyLoanPayments + input.PropertyTax) * input.DurationOfLoan);

            var table = this.PrepareTable(input);

            int middleDuration = input.DurationOfLoan / 2;
            bool stopRequested = false;
            string stopRequestedMessage = string.Empty;

            bool totalInterestPaidWarning = false;
            var warnings = new List<string>();

            table.AfterCellValueComputed += delegate(object sender, TableEventArgs<int, string, MortgageCalculatorOperationContext> e)
            {
                var currentCell = e.OperationContext.CurrentCell;

                if (currentCell.ColumnId == AmortizationTableColumns.OpeningBalance)
                {
                    this.CalculateAfterCellValueComputedOpeningBalance(e, middleDuration, input.MaxBalanceAtMiddleDuration, ref stopRequestedMessage, ref stopRequested);
                }

                if (currentCell.ColumnId == AmortizationTableColumns.Interest)
                {
                    if (!totalInterestPaidWarning)
                    {
                        totalInterestPaidWarning = this.CalculateAfterCellValueComputedInterest(e, input.MaxInterestPaidWarning, warnings);
                    }
                }
            };

            var computeResult = table.Compute();
            if (stopRequested)
            {
                output.Error = stopRequestedMessage;
                return output;
            }

            if (!computeResult)
            {
                output.Error = table.OperationContext.LastOperationResult.Message;
                return output;
            }

            output.TotalLoanPayments = Decimal.Round(table.OperationContext.TotalLoanPayments);
            output.TotalInterestPaid = Decimal.Round(table.OperationContext.TotalInterestPaid);
            output.SetAmortizationTable(table);

            if (warnings.Count > 0)
            {
                output.Warnings = warnings;
            }

            table.SaveToCsv(CsvFile, true, AmortizationTableColumns.ColumnsForCsvFile);
            table.SaveToXml(XmlFile);

            return output;
        }

        public InterestRateCalculationOutput CalculateInterestRateByPaidInterest(MaxInterestRateCalculatorInput input, decimal minInterestRate, decimal maxInterestRate)
        {
            // try with min interest rate first
            string message;
            bool canUseInterestRange = this.CanUseInterestRange(input, minInterestRate, out message);
            if (!canUseInterestRange)
            {
                return new InterestRateCalculationOutput { Message = message };
            }

            int count = 0;
            int maxIterations = 30;
            decimal lastGoodInterest = 0;
            while (count < maxIterations)
            {
                count++;
                System.Diagnostics.Debug.WriteLine("Iteration: {0}", count);

                input.InterestRate = Math.Round(minInterestRate + (maxInterestRate - minInterestRate) / 2, 6);
                
                decimal totalInterestPaid;
                bool interestRateCalculated = this.CalculateInterestRate(input, out message, out totalInterestPaid);
                if (!interestRateCalculated)
                {
                    return new InterestRateCalculationOutput { Message = message };
                }

                // ensures that the interest returned produces a TotalInterestPaid smaller than MaxInterestPaid
                if (totalInterestPaid <= input.MaxInterestPaid)
                {
                    lastGoodInterest = input.InterestRate;
                }

                if (totalInterestPaid == input.MaxInterestPaid || maxInterestRate - minInterestRate < .0001m)
                {
                    System.Diagnostics.Debug.WriteLine("Iteration count: {0}", count);
                    return new InterestRateCalculationOutput { InterestRate = Math.Round(lastGoodInterest, 4) };
                }

                if (totalInterestPaid > input.MaxInterestPaid)
                {
                    maxInterestRate = input.InterestRate;
                }
                else
                {
                    minInterestRate = input.InterestRate;
                }

                Thread.Sleep(1);
            }

            return new InterestRateCalculationOutput { Message = "Too many iterations." };
        }

        private bool CalculateAfterCellValueComputedInterest(
            TableEventArgs<int, string, MortgageCalculatorOperationContext> e,
            decimal maxInterestPaidWarning,
            List<string> warnings)
        {
            if (e.OperationContext.TotalInterestPaid > maxInterestPaidWarning)
            {
                string warning = string.Format(
                    "Max Interest Paid [{0}] reached at payment number {1}",
                    maxInterestPaidWarning.ToStringCurrency(),
                    e.OperationContext.CurrentCell.RowId);

                warnings.Add(warning);
                return true;
            }

            return false;
        }

        private void CalculateAfterCellValueComputedOpeningBalance(
            TableEventArgs<int, string, MortgageCalculatorOperationContext> e,
            int middleDuration,
            decimal maxBalanceAtMiddleDuration,
            ref string stopRequestedMessage,
            ref bool stopRequested)
        {
            var currentCell = e.OperationContext.CurrentCell;
            if (currentCell.RowId == middleDuration)
            {
                var currentValue = currentCell.GetValueAsDecimal();
                if (currentValue > maxBalanceAtMiddleDuration)
                {
                    stopRequestedMessage = string.Format(
                        "The Opening Balance At Middle Duration [{0}] is greater than Max Balance At Middle Duration [{1}]",
                        currentValue.ToStringCurrency(),
                        maxBalanceAtMiddleDuration.ToStringCurrency());

                    e.CanContinue = false;
                    stopRequested = true;
                }
            }
        }

        private Table<int, string, MortgageCalculatorOperationContext> PrepareTable(IMortgageCalculatorInput input)
        {
            var rowResolver = new RowResolver<int>(Enumerable.Range(1, input.DurationOfLoan).ToList());
            var colResolver =
                new ColumnResolver<string>(AmortizationTableColumns.ColumnsWithOrderOfOperations);
            var context = new MortgageCalculatorOperationContext { Input = input };

            var table = new Table<int, string, MortgageCalculatorOperationContext>(
                rowResolver,
                colResolver,
                context,
                calculatorFactory);

            table.SetColumn(AmortizationTableColumns.PropertyTax, input.PropertyTax);
            table.SetColumnWithFormula(AmortizationTableColumns.PaymentNumber, (ctx) =>
            {
                return CellOperationResult.CreateSucceed(ctx.CurrentCell.RowId);
            });

            table.SetColumnWithFormula(AmortizationTableColumns.RemainingPayments, (ctx) =>
            {
                return CellOperationResult.CreateSucceed(ctx.Input.DurationOfLoan - ctx.CurrentCell.RowId);
            });

            table.SetColumnWithFormula(AmortizationTableColumns.PaymentDate, (ctx) =>
            {
                return CellOperationResult.CreateSucceed(ctx.Input.LoanStartDate.AddMonths(ctx.CurrentCell.RowId - 1));
            });

            //old way without the class factory 
            //table.SetColumnWithCalculator<InterestCalculator>(AmortizationTableColumns.Interest);

            table.SetColumnWithCalculatorFromFactory<IInterestCalculator>(AmortizationTableColumns.Interest);
            table.SetColumnWithCalculatorFromFactory<IPrincipalCalculator>(AmortizationTableColumns.Principal);
            table.SetColumnWithCalculatorFromFactory<ITotalPaymentsCalculator>(AmortizationTableColumns.TotalPayments);
            table.SetColumnWithCalculatorFromFactory<IClosingBalanceCalculator>(AmortizationTableColumns.ClosingBalance);
            table.SetColumnWithCalculatorFromFactory<IOpeningBalanceCalculator>(AmortizationTableColumns.OpeningBalance);

            return table;
        }

        private CalculatorFactory<int, string, MortgageCalculatorOperationContext> CreateCalculatorFactory()
        {
            var calculatorFactory = new CalculatorFactory<int, string, MortgageCalculatorOperationContext>();
            calculatorFactory.Register<IInterestCalculator, InterestCalculator>();
            calculatorFactory.Register<IPrincipalCalculator, PrincipalCalculator>();
            calculatorFactory.Register<ITotalPaymentsCalculator, TotalPaymentsCalculator>();
            calculatorFactory.Register<IClosingBalanceCalculator, ClosingBalanceCalculator>();
            calculatorFactory.Register<IOpeningBalanceCalculator, OpeningBalanceCalculator>();
            return calculatorFactory;
        }

        private bool CanUseInterestRange(MaxInterestRateCalculatorInput input,
            decimal minInterestRate,
            out string message)
        {
            input.InterestRate = minInterestRate;
            decimal totalInterestPaid;

            if (!this.CalculateInterestRate(input, out message, out totalInterestPaid))
            {
                return false;
            }

            if (totalInterestPaid > input.MaxInterestPaid)
            {
                message = "Impossible with the interest rate range.";
                return false;
            }

            message = string.Empty;
            return true;
        }

        private bool CalculateInterestRate(MaxInterestRateCalculatorInput input, out string message, out decimal totalInterestPaid)
        {
            System.Diagnostics.Debug.WriteLine("Using Interest Rate: {0}", input.InterestRate);

            var table = this.PrepareTable(input);
            bool computeResult = table.Compute();
            totalInterestPaid = table.OperationContext.TotalInterestPaid;
            if (!computeResult)
            {
                message = table.OperationContext.LastOperationResult.Message;
                return false;
            }

            //table.SaveToCsv(@"c:\temp\nt.csv");
            message = string.Empty;
            return true;
        }
    }
}
